/*
gcc -o GDAL_drv.so \
    -I/usr/local/lib/erlang/usr/include/ \
    -fpic -shared \
    GDAL_drv.c spectrometer.c \
    -lpthread -lgdal      
    
gcc -o GDAL_drv.so -fpic -shared -flat_namespace -undefined suppress -fno-common -I/usr/local/lib/erlang/usr/include -L/usr/local/lib/erlang/usr/lib/ GDAL_drv.c spectrometer.c -D MAC_OS_X -framework GDAL

*/
#include <stdio.h>
#include <string.h>
#include <errno.h>

#include <unistd.h>
#include <fcntl.h>
#include <ctype.h>

#include "ai_image.h"
#include "erl_driver.h"

#ifdef MAC_OS_X
    #include <GDAL/gdal.h>
    #include <GDAL/cpl_string.h>
#else
    #include "gdal.h"
    #include "cpl_string.h"
#endif

extern GDALDatasetH createMemoryDataset(char *file_name, int nBands, int nXSize, int nYSize);
    

#define get_int32(s) ((((unsigned char*) (s))[0] << 24) | \
                      (((unsigned char*) (s))[1] << 16) | \
                      (((unsigned char*) (s))[2] << 8)  | \
                      (((unsigned char*) (s))[3]))

#define put_int32(i, s) {((char*)(s))[0] = (char)((i) >> 24) & 0xff; \
                        ((char*)(s))[1] = (char)((i) >> 16) & 0xff; \
                        ((char*)(s))[2] = (char)((i) >> 8)  & 0xff; \
                        ((char*)(s))[3] = (char)((i)        & 0xff);}

/* op codes */
#define XX_OPEN             'o'
#define XX_OPEN_WITH_IMG    'm'
#define XX_CALC_HISTOGRAM   'h'
#define XX_READ             'r'
#define XX_WRITE            'w'
#define XX_SPLIT_LIGHT      's'
#define XX_TO_PNG           'p'
#define XX_GET_PNG          'g'


/* return codes */
#define XX_VALUE        'v'
#define XX_OK           'o'
#define XX_FERROR       'E'
#define XX_REOF         'x'
#define XX_BANDS        'S'
#define XX_PNG          'P'
#define XX_HISTOGRAM    'H'

#define RED     1
#define GREEN   2
#define BLUE    3

static ErlDrvData GDAL_start(ErlDrvPort port, char *buf);
static void GDAL_stop(ErlDrvData drv_data);

static ErlDrvEntry GDAL_driver_entry;

#define HISTOGRAM_LENGHT    256
#define NAME_SIZE 256

typedef struct _desc {
    ErlDrvPort port;
    FILE *log;
    char image_name[NAME_SIZE];
    GDALDatasetH dataset;
} Desc;

static ErlDrvData GDAL_start(ErlDrvPort port, char *buf) {
    Desc *d = (Desc*) driver_alloc(sizeof (Desc));

    if (d == NULL)
        return (ErlDrvData) -1;
    d->dataset = NULL;
    d->log = NULL;
d->log = fopen("./logs/GDAL_drv.log", "a");
fprintf(d->log, "GDAL_start ing\n");
fflush(d->log);
    memset(d->image_name, 0, NAME_SIZE);
    d->port = port;
    set_port_control_flags(port, PORT_CONTROL_FLAG_BINARY);

    return (ErlDrvData) d;
}

static void GDAL_stop(ErlDrvData drv_data) {
    Desc *d = (Desc*) drv_data;
    if (d->log != NULL) {
fprintf(d->log, "GDAL_drv stop, for %s\n", d->image_name);
        fclose(d->log);
        d->log = NULL;
    }
    if (d->dataset != NULL) {
        GDALClose(d->dataset);
        d->dataset = NULL;
    }
    driver_free(d);
}

static char *driver_error(ErlDrvPort port, int err) {
    char response[256];     /* Response buffer. */
    char* s;
    char* t;
    ErlDrvBinary* bin;

    bin = driver_alloc_binary(1 + strlen (erl_errno_id (err)));
    bin->orig_bytes[0] = XX_FERROR;

    response[0] = XX_FERROR;
    for (s = erl_errno_id(err), t = bin->orig_bytes + 1; *s; s++, t++)
    *t = tolower(*s);
    return (char *)bin;
}

static char *driver_ok(ErlDrvPort port) {
    char ch = XX_OK;
    ErlDrvBinary* bin;
    bin = driver_alloc_binary(1);
    bin->orig_bytes[0] = ch;
    return (char *)bin;
}

static char *put_band(band *pBand, char* pBin) {
    put_int32(pBand->xSize, pBin);
    put_int32(pBand->ySize, pBin + 4);
    int band_size = pBand->xSize * pBand->ySize * sizeof(byte);
    memcpy(pBin + 8, pBand->data, band_size);
    return pBin + 8 + band_size;
}

static byte *write_band(GDALDatasetH dataset, byte *pBand, int nthBand) {
    int bandXSize = get_int32(pBand);
    int bandYSize = get_int32(pBand + 4);
    copyToMemory(dataset, pBand + 8, nthBand, bandXSize, bandYSize);
    return pBand + 8 + bandXSize * bandYSize * sizeof(byte);
}

static int GDAL_control(ErlDrvData drv_data,
            unsigned int command,
            char *buf, int len,
            char **rbuf, int rlen) 
{
    Desc *desc = (Desc*) drv_data;
    ErlDrvBinary* bin;
    switch (command) {
        case XX_OPEN_WITH_IMG: {
            if (desc->dataset != NULL) {
fprintf(desc->log, "ERROR open with img: %s\t", desc->image_name);
fflush(desc->log);
                *rbuf = driver_error(desc->port, EINVAL);
                return 1;
            }
fprintf(desc->log, "open with img:\t");
            int filename_len = (int)buf[0];
            memset(desc->image_name, 0, NAME_SIZE);
            memcpy(desc->image_name, buf+1, filename_len);
fprintf(desc->log, "%s\n", desc->image_name);

            byte *imgBuf = buf + 1 + filename_len;
            int img_len = len - (1 + filename_len);
fprintf(desc->log, "img len: %ld\n", img_len);
            band band1, band2, band3;
            spectrometer(desc->image_name, imgBuf, img_len, &band1, &band2, &band3);
            int band1_size = band1.xSize * band1.ySize * sizeof(byte);
            int band2_size = band2.xSize * band2.ySize * sizeof(byte);
            int band3_size = band3.xSize * band3.ySize * sizeof(byte);
fprintf(desc->log, "size: band1=%d, band2=%d, band3=%d\n", band1_size, band2_size, band3_size);
fflush(desc->log);

            GDALDatasetH dataset = createMemoryDataset(desc->image_name, 3, band1.xSize, band1.ySize);
            copyToMemory(dataset, band1.data, 1, band1.xSize, band1.ySize);
            copyToMemory(dataset, band2.data, 2, band2.xSize, band2.ySize);
            copyToMemory(dataset, band3.data, 3, band3.xSize, band3.ySize);
 
            desc->dataset = dataset;

            *rbuf = driver_ok(desc->port);
            return 1;
        }
        case XX_OPEN: {
fprintf(desc->log, "openning\n");            
            //char image_name[BUFSIZ];  /* should be FILENAME_MAX */
            char* src;
            char* dst;
            char* src_end;
            char* dst_end;
//            if (desc->image_name != NULL) {
//                *rbuf = driver_error(desc->port, EINVAL);
//                return 1;
//            }

            src = buf;
            src_end = buf + len;

          	/* get image name */
            dst = desc->image_name;
            dst_end = dst + NAME_SIZE; /* make room for a '\0' */
            while((src < src_end) && (dst < dst_end) && (*src != '\0'))
                *dst++ = *src++;
            if ((src == src_end) || (dst == dst_end)) {
                driver_error(desc->port, EINVAL);
            }
            *dst = *src++;
fprintf(desc->log, "name: %s, len:%d NAME_SIZE=%d\n", desc->image_name, strlen(desc->image_name), NAME_SIZE);
fflush(desc->log);            

            *rbuf = driver_ok(desc->port);
            return 1;
        }
        case XX_CALC_HISTOGRAM: {
            char ch = XX_HISTOGRAM;
            if (desc->dataset == NULL || desc->image_name == NULL) {
                *rbuf = driver_error(desc->port, -1);
                return 1;				
			}
            int nBand = (int)buf[0];
//fprintf(desc->log, "XX_CALC_HISTOGRAM -> band: %d\n", nBand);
            GDALRasterBandH hBand = GDALGetRasterBand( desc->dataset, nBand );
            if (hBand == NULL) {
                *rbuf = driver_error(desc->port, -1);
                return 1;
            }
            double min, max;
            int nBuckets;//HISTOGRAM_LENGHT;
            int *res_bin = NULL;
            CPLErr eErr =
            GDALGetDefaultHistogram(hBand, &min, &max, &nBuckets, &res_bin, TRUE, GDALDummyProgress, NULL);
//fprintf(desc->log, "min=%f, max=%f, nBuckets=%d\n", min, max, nBuckets);
//fflush(desc->log);
            
            if ((bin = driver_alloc_binary(nBuckets * sizeof(int) + 1)) == NULL) {
fprintf(desc->log, "Error -> driver_alloc_binary:%d\n", nBuckets);
fflush(desc->log);
                CPLFree(res_bin);
                *rbuf = driver_error(desc->port, -1);
                return 1;
            }
            bin->orig_bytes[0] = ch;
            //memcpy(bin->orig_bytes + 1, res_bin, nBuckets * sizeof(int));
            int i;
            char *pBuf = bin->orig_bytes + 1;
            for(i=0; i<nBuckets; ++i) {
                put_int32(res_bin[i], pBuf + 4*i);
//fprintf(desc->log, "%d, ", res_bin[i]);
                
            }
//fflush(desc->log);
            CPLFree(res_bin);
         
            *rbuf = (char *)bin;
            return 1;
        }
        case XX_SPLIT_LIGHT: {
fprintf(desc->log, "XX_SPLIT_LIGHT -> name: %s\n", desc->image_name);
fflush(desc->log);            
            band band1, band2, band3;
            spectrometer(desc->image_name, buf, len, &band1, &band2, &band3);
            int band1_size = band1.xSize * band1.ySize * sizeof(byte);
            int band2_size = band2.xSize * band2.ySize * sizeof(byte);
            int band3_size = band3.xSize * band3.ySize * sizeof(byte);
//fprintf(desc->log, "size: band1=%d, band2=%d, band3=%d\n", band1_size, band2_size, band3_size);
//fflush(desc->log);
            
        	char ch = XX_BANDS;
        	int rval;
        	int sz = 4 + 4 + band1_size
        	        + 4 + 4 + band2_size 
        	         + 4 + 4 + band3_size
        	        ;     //get_int32(buf);
        
        	if ((bin = driver_alloc_binary(sz + 1)) == NULL) {
        	    *rbuf = driver_error(desc->port, -1);
        	    return 1;
        	}
        
        	bin->orig_bytes[0] = ch;
            char *pBin = NULL;
            pBin = put_band(&band1, bin->orig_bytes+1);
            pBin = put_band(&band2, pBin);
            pBin = put_band(&band3, pBin);
            
            free_band_data(&band1),
            free_band_data(&band2),
            free_band_data(&band3),
            
        	*rbuf = (char *)bin;
fprintf(desc->log, "XX_SPLIT_LIGHT -> OVER: %s\n", desc->image_name);
fflush(desc->log);            
        	return 1;
        }
		case XX_GET_PNG: {
            char ch = XX_PNG;
            if (desc->dataset == NULL || desc->image_name == NULL) {
                *rbuf = driver_error(desc->port, -1);
                return 1;				
			}
            GDALDriverH hDriver = GDALGetDriverByName( "PNG" );
            char out_filename[256] = "/vsimem/work.out";
            strcat(out_filename, desc->image_name);
            strcat(out_filename, buf);
            strcat(out_filename, ".out");
//fprintf(desc->log, "out file: %s\n", out_filename);
//fflush(desc->log);      			
            char **papszOptions = NULL;
//          papszOptions = CSLSetNameValue( papszOptions, "COMPRESS", "PACKBITS" );
            GDALDatasetH hOutDS = NULL;
            hOutDS = GDALCreateCopy( hDriver,out_filename, desc->dataset, TRUE, papszOptions, 
                                    NULL, NULL );
            if (hOutDS == NULL) {
                *rbuf = driver_error(desc->port, -1);
                return 1;
            }                
            GDALClose(hOutDS);
            vsi_l_offset outDataLength;
            GByte * binData = VSIGetMemFileBuffer(out_filename, &outDataLength, TRUE); 
//fprintf(desc->log, "PNG -> rlen: %d\n", rlen);
//fprintf(desc->log, "PNG -> opts: %s\n", GDALGetDriverCreationOptionList(hDriver));
//fflush(desc->log);      			
            if (binData == NULL) {
                *rbuf = driver_error(desc->port, -1);
                return 1;
            }
            if ((bin = driver_alloc_binary(outDataLength + 1)) == NULL) {
                *rbuf = driver_error(desc->port, -1);
                return 1;
			}
            bin->orig_bytes[0] = ch;
            memcpy(bin->orig_bytes + 1, binData, outDataLength);
//			VSIUnlink( out_filename );  //bUnlinkAndSeize==TRUE, so the file will be auto removed
            CPLFree(binData);
            *rbuf = (char *)bin;
//            *rbuf = driver_ok(desc->port);
            return 1;
		}
        case XX_TO_PNG: {
fprintf(desc->log, "XX_TO_PNG -> name: %s\n", desc->image_name);
fflush(desc->log);            
//fprintf(desc->log, "TO PNG: len=%d\n", len);
//fflush(desc->log);
            int filename_len = (int)buf[0];
            int i=0;
            char save_name[256];
            memset(save_name, 0, 256);
            memcpy(save_name, buf+1, filename_len);
            char *bandBuf = buf + 1 + filename_len;
            int band1_xsize = get_int32(bandBuf);
            int band1_ysize = get_int32(bandBuf + 4);

            GDALDatasetH dataset = createMemoryDataset(desc->image_name, 3, band1_xsize, band1_ysize);
            byte *pBin = write_band(dataset, bandBuf, 1);
            pBin = write_band(dataset, pBin, 2);
            pBin = write_band(dataset, pBin, 3);

            saveAsPngFile(dataset, save_name);
            GDALClose(dataset);
            *rbuf = driver_ok(desc->port);
fprintf(desc->log, "XX_TO_PNG -> OVER: %s\n", desc->image_name);
fflush(desc->log);            
            return 1;
        }
    }
    return 1;
}

static int GDAL_init(void) {
FILE *fp = fopen("./logs/GDAL_drv.log", "a");
fprintf(fp, "GDAL_init ing\n");
fclose(fp);
    GDALAllRegister();
    return 0;
}

static void GDAL_finish()
{
FILE *fp = fopen("./logs/GDAL_drv.log", "a");
fprintf(fp, "GDAL_finish ing\n");
fclose(fp);
    /*
     * Make sure any remaining buffers are flushed (this is done on exit() by
     * the normal stdio).
     */
//    bbio_cleanup();
}

/*
 * Initialize and return a driver entry struct
 */

DRIVER_INIT(GDAL_drv)
{
    GDAL_driver_entry.init         = GDAL_init;   /* Not used */
    GDAL_driver_entry.start        = GDAL_start;
    GDAL_driver_entry.stop         = GDAL_stop;
    GDAL_driver_entry.output       = NULL;
    GDAL_driver_entry.ready_input  = NULL;
    GDAL_driver_entry.ready_output = NULL;
    GDAL_driver_entry.driver_name  = "GDAL_drv";
    GDAL_driver_entry.finish       = GDAL_finish;
    GDAL_driver_entry.outputv      = NULL;
    GDAL_driver_entry.control      = GDAL_control;
    return &GDAL_driver_entry;
}


